﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Threading;

/*=====================================================================
  This file is part of a Microsoft SQL Server Shared Source Application.
  Copyright (C) Microsoft Corporation.  All rights reserved.
 
THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
PARTICULAR PURPOSE.
======================================================= */


namespace Microsoft.SqlCommunity.ADONETMirroring
{
    /// <summary>
    /// Part of the implementation of a custom .NET Framework Data Provider which wraps the SQL Server data provider.
    /// This custom data provider  retries operations which 
    /// fail to enable automatic failover to a mirror server.  
    /// 
    /// This class represents the connection to the principal/mirror server pair.  All operations eventually pass through
    /// to the underlying SqlConnection.  However, retry logic is wrapped around those calls which might fail due to the principal going
    /// down.
    /// 
    /// If only SqlConnection/SqlCommand were not sealed!
    /// </summary>
    public class MirrorConnection : IDbConnection
    {
        /// <summary>
        /// This event is raised if the SqlConnection underlying this connection is closed because of a break in communication with
        /// the current principal.
        /// </summary>
        internal event EventHandler<EventArgs> InternalConnectionReset;

        private int _maxRetries = 3;  //How many times to retry a command if an error occurs until we give up.

        /// <summary>
        /// How many times to retry a command if an error occurs until we give up.
        /// </summary>
        public int MaxRetries
        {
            get { return _maxRetries; }
            set { _maxRetries = value; }
        }

        /// <summary>
        /// How long in seconds to wait before retrying, with exponential roll off.
        /// </summary>
        private double _delayBase = 2.0d;
        public double DelayBase
        {
            get { return _delayBase; }
            set { _delayBase = value; }
        }

        // The relevant error codes which are documented in http://support.microsoft.com/kb/109787
        // which seem to be worthwhile for retrying.  If errors occur which are not in this list they 
        // are simply rethrown.
        readonly static int[] RetryErrorNumbers = new int[] 
        { 
             1608,  // A network error was encountered while sending results to the front end
            10008,  // Bad token from SQL Server: datastream processing out of sync
            10010,  // Read from SQL Server failed
            10018,  // Error closing network connection
            10025,  // Write to SQL Server failed
            10053,  // Software caused connection abort
            10054,  // Connection reset by peer
            10058,  // Can't send after socket shutdown
            17824,  // Unable to write to server-side connection
            17825,  // Unable to close server-side connection
            17832,  // Unable to read login packet(s)            
        };

        /// <summary>
        /// The underlying connection to the principal/mirror server pair.
        /// </summary>
        private SqlConnection _connection;

        /// <summary>
        ///  The underlying connection to the principal/mirror server pair. 
        /// </summary>
        internal SqlConnection InnerConnection
        {
            get { return _connection; }
            set { _connection = value; }
        }

        /// <summary>
        /// Used to hold the delegate which will be invoked when the underlying connection changes state.
        /// </summary>
        private StateChangeEventHandler _connectionStateChangeHandler;

        /// <summary>
        /// Use this constructor if the connection string will be set later.
        /// </summary>
        public MirrorConnection()
        {
            _connection = new SqlConnection();
        }

        // Use this constructor if the connection string is available before instantiating the connection.
        public MirrorConnection(string connectionString)
        {
            _connection = new SqlConnection(connectionString);
        }

        /// <summary>
        /// What server and database to use, and how to connect to them.
        /// </summary>
        public string ConnectionString
        {
            get
            {
                return _connection.ConnectionString;
            }
            set
            {
                _connection.ConnectionString = value;
            }
        }

        /// <summary>
        /// How long to wait until it is assumed that something has gone wrong in connecting to the server.
        /// </summary>
        public int ConnectionTimeout
        {
            get
            {
                return _connection.ConnectionTimeout;
            }
        }

        /// <summary>
        /// Which database to access
        /// </summary>
        public string Database
        {
            get
            {
                return _connection.Database;
            }
        }

        /// <summary>
        /// The status of the connection
        /// </summary>
        public ConnectionState State
        {
            get { return _connection.State; }
        }

        /// <summary>
        /// Initiates a transaction on the connection.  Note that we don't encapsulate SqlTransaction, we just
        /// return it as is.
        /// </summary>
        /// <returns>The object which represents and can manipulate the transaction</returns>
        public IDbTransaction BeginTransaction()
        {
            return WithMirrorRetry<IDbTransaction>(() => _connection.BeginTransaction());
        }

        /// <summary>
        /// Initiates a transaction on the connection with the specified isolation level
        /// </summary>
        /// <param name="level">How isolated this transactions is from other transactions which may be executing in parallel</param>
        /// <returns>The object which represents and can manipulate the transaction</returns>
        public IDbTransaction BeginTransaction(IsolationLevel level)
        {
            return WithMirrorRetry<IDbTransaction>(() => _connection.BeginTransaction(level));
        }

        /// <summary>
        /// Initiates a transaction on the connection with the specified name
        /// </summary>
        /// <param name="transactionName">What to call the transaction</param>
        /// <returns>The object which represents and can manipulate the transaction</returns>
        public IDbTransaction BeginTransaction(string transactionName)
        {
            return WithMirrorRetry<IDbTransaction>(() => _connection.BeginTransaction(transactionName));
        }

        /// <summary>
        /// Initiates a transaction on the connection with the specified isolation level and name
        /// </summary>
        /// <param name="level">How isolated this transactions is from other transactions which may be executing in parallel</param>
        /// <param name="transactionName">What to call the transaction</param>
        /// <returns>The object which represents and can manipulate the transaction</returns>
        public IDbTransaction BeginTransaction(IsolationLevel level, string transactionName)
        {
            return WithMirrorRetry<IDbTransaction>(() => _connection.BeginTransaction(level, transactionName));
        }

        /// <summary>
        /// Update the name of the database to access with this connection.
        /// </summary>
        /// <param name="databaseName">Which database to access</param>
        public void ChangeDatabase(string databaseName)
        {
            WithMirrorRetry(() => {_connection.ChangeDatabase(databaseName); });
        }

        /// <summary>
        /// Open the connection to the principal server, retrying as necessary.
        /// </summary>
        public void Open()
        {
            WithMirrorRetry(() => {_connection.Open();});
            // Make sure we find out if the underlying connection changes state (in particular, if it becomes closed).
            // This can happen if the principal goes down after the connection has been established.
            _connectionStateChangeHandler = new StateChangeEventHandler(ConnectionStateChangeHandler);
            _connection.StateChange += _connectionStateChangeHandler;
        }

        /// <summary>
        /// The method that gets called when the event is raised when the state of the underlying connection changes.
        /// </summary>
        /// <param name="sender">The SqlConnection who's state has changed</param>
        /// <param name="e">Information about the change in state</param>
        void ConnectionStateChangeHandler(object sender, StateChangeEventArgs e)
        {
            if (_connection.State == ConnectionState.Broken || _connection.State == ConnectionState.Closed)
            {
                // If the state changed to broken or closed it could be because the principal has gone down.
                // Create a new underlying connection, open it, and raise the InternalConnectionReset event so that
                // internal components which need to know about the connection change can update their state.
                _connection = new SqlConnection(_connection.ConnectionString);
                WithMirrorRetry(() => { _connection.Open(); });
                if (InternalConnectionReset != null)
                    InternalConnectionReset(this, new EventArgs());
            }
        }

        /// <summary>
        /// Shutdown the connection.  Since we're closing the underlying connection on purpose, it is important to 
        /// remove the handler for the underlying connection's state change event.
        /// </summary>
        public void Close()
        {
            _connection.StateChange -= _connectionStateChangeHandler;
            WithMirrorRetry(() => { _connection.Close(); });
        }

        /// <summary>
        /// Create a command object and associate it with this connection.
        /// </summary>
        /// <returns></returns>
        public IDbCommand CreateCommand()
        {
            MirrorCommand command = new MirrorCommand();
            command.Connection = this;
            return command;

        }

        /// <summary>
        /// Implement the standard dispose pattern.
        /// </summary>
        void IDisposable.Dispose()
        {
            this.Dispose(true);
            System.GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Implement the standard dispose pattern.  Be sure to close the underlying connection.
        /// </summary>
        private void Dispose(bool disposing)
        {
            if (_connection.State == ConnectionState.Open)
                this.Close();
        }

        /// <summary>
        /// This delegate is used to represent the method (which returns no values) to call and to retry the call on
        /// in case certain recoverable errors occur.  
        /// </summary>
        internal delegate void RetryBody();

        /// <summary>
        /// This method invokes the supplied delegate with retry logic wrapped around it.  No values are returned.  If the delegate raises 
        /// recoverable SQL server or client errors, then the supplied delegate is reinvoked after a certain amount of delay
        /// (which grows exponentially for each retry) until the retry limit is exceeded, at which point the exception
        /// is rethrown.  Other exceptions are not caught and will be visible to callers.
        /// </summary>
        /// <param name="body">The delegate to invoke within the retry code.</param>
        internal void WithMirrorRetry (RetryBody body)
        {
            int retryCount = 0;

            while (true)
            {
                try
                {
                    body();
                    return;
                }
                catch (SqlException se)
                {
                    if (!RetryErrorNumbers.Contains<int>(se.Number))
                        throw;
                    else
                        if (retryCount >= _maxRetries) throw;
                }
                //Sleep exponentially more each time through the retry loop
                Thread.Sleep((int)Math.Pow(_delayBase, retryCount));
                retryCount += 1;
            }
        }

        /// <summary>
        /// Similar to the above method, but a return value is allowed from the delegate.
        /// </summary>
        /// <typeparam name="TReturn">The type of object being returned</typeparam>
        /// <param name="body">The delegate to invoke within the retry logic which will produce the value to return</param>
        /// <returns>The value the delegate returns</returns>
        internal TReturn WithMirrorRetry<TReturn>(Func<TReturn> body)
        {
            int retryCount = 0;

            while (true)
            {
                try
                {
                    return body();
                }
                catch (SqlException se)
                {
                    if (!RetryErrorNumbers.Contains<int>(se.Number))
                        throw;
                    else
                        if (retryCount >= _maxRetries) throw;
                }
                //Sleep exponentially more each time through the retry loop
                Thread.Sleep((int)Math.Pow(_delayBase, retryCount));
                retryCount += 1;
            }
        }

    }
}
    

